using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using sqengine.Components;
using SqEngine;

namespace sqengine
{
    /// <summary>
    /// First person camera component for the demos, rotated by mouse.
    /// </summary>
    public class Camera : GameComponent
    {
        private Matrix view;
        private Matrix projection;
        private Matrix cameraRotation;

        public Matrix viewMatrix;
        private Matrix projMatrix;
        public Quaternion orientation;


        private Vector3 position = new Vector3(0, 0, 10);
        public Vector2 angles = Vector2.Zero;

        private int widthOver2;
        private int heightOver2;

        private Vector3 xAxis;
        private Vector3 yAxis;
        private Vector3 zAxis;
        private Vector3 viewDir;

        private float fovx;
        private float znear;


        private float zfar;
        private Vector3 eye;
        private Vector3 target;

        private float accumHeadingDegrees;
        private float accumPitchDegrees;
        private int mouseIndex;
        private Vector2[] mouseMovement;
        private Vector2 smoothedMouseMovement;

        private MouseState previousMouseState;
        private MouseState currentMouseState;


        private float fieldOfView = Microsoft.Xna.Framework.MathHelper.PiOver4;
        private float aspectRatio;
        private float nearPlaneDistance = 0.1f;
        private float farPlaneDistance = 10000.0f;
        public Viewport GameViewport;

        public bool EnableMouseInput { get; set; }
        public bool EnableKeyboardInput { get; set; }
        public bool IsMousePinned { get; set; }
        private bool isWireframeEnabled = false;

        private KeysManager _keymanager;

        KeyboardState kb_status;
        KeyboardState prev_kb_status;

        Game _game;

        private static Vector3 WORLD_X_AXIS = new Vector3(1.0f, 0.0f, 0.0f);
        private static Vector3 WORLD_Y_AXIS = new Vector3(0.0f, 1.0f, 0.0f);
        private static Vector3 WORLD_Z_AXIS = new Vector3(0.0f, 0.0f, 1.0f);


        private MouseState prevMouseState = new MouseState();

        /// <summary>
        /// Initializes new camera component.
        /// </summary>
        /// <param name="game">Game to which attach this camera.</param>
        public Camera(Game game, KeysManager keyman)
            : base(game)
        {

            accumHeadingDegrees = 0.0f;
            accumPitchDegrees = 0.0f;

            eye = Vector3.Zero;
            target = Vector3.Zero;

            widthOver2 = game.Window.ClientBounds.Width / 2;
            heightOver2 = game.Window.ClientBounds.Height / 2;
            aspectRatio = (float)game.Window.ClientBounds.Width / (float)game.Window.ClientBounds.Height;

            mouseIndex = 0;
            mouseMovement = new Vector2[2];
            mouseMovement[0].X = 0.0f;
            mouseMovement[0].Y = 0.0f;
            mouseMovement[1].X = 0.0f;
            mouseMovement[1].Y = 0.0f;

            UpdateProjection();

            Mouse.SetPosition(widthOver2, heightOver2);

            IsMousePinned = true;
            EnableKeyboardInput = true;
            EnableMouseInput = true;
            _game = game;
            _keymanager = keyman;
        }

        public float HeadingDegrees
        {
            get { return -accumHeadingDegrees; }
        }

        public Quaternion QOrientation
        {
            get { return orientation; }
        }

        public float PitchDegrees
        {
            get { return -accumPitchDegrees; }
        }

        public Matrix ViewProjectionMatrix
        {
            get { return viewMatrix * projMatrix; }
        }

        /// <summary>
        /// Gets camera view matrix.
        /// </summary>
        public Matrix View { get { return view; } }

        public Matrix Orientation { get { return cameraRotation; } }

        public Matrix ProjectionMatrix { get { return projMatrix; } set { projMatrix = value; } }

        /// <summary>
        /// Gets or sets camera projection matrix.
        /// </summary>        
        public Matrix Projection { get { return projection; } set { projection = value; } }
        /// <summary>
        /// Gets camera view matrix multiplied by projection matrix.
        /// </summary>
        public Matrix ViewProjection { get { return view * projection; } }
        /// <summary>
        /// Gets or sets camera position.
        /// </summary>
        public Vector3 Position { get { return position; } set { position = value; } }

        /// <summary>
        /// Gets or sets camera field of view.
        /// </summary>
        public float FieldOfView { get { return fieldOfView; } set { fieldOfView = value; UpdateProjection(); } }
        /// <summary>
        /// Gets or sets camera aspect ratio.
        /// </summary>
        public float AspectRatio { get { return aspectRatio; } set { aspectRatio = value; UpdateProjection(); } }
        /// <summary>
        /// Gets or sets camera near plane distance.
        /// </summary>
        public float NearPlaneDistance { get { return nearPlaneDistance; } set { nearPlaneDistance = value; UpdateProjection(); } }
        /// <summary>
        /// Gets or sets camera far plane distance.
        /// </summary>
        public float FarPlaneDistance { get { return farPlaneDistance; } set { farPlaneDistance = value; UpdateProjection(); } }

        public Vector2 Angles { get { return angles; } }

        /// <summary>
        /// Gets or sets camera's target.
        /// </summary>
        public Vector3 Target
        {
            get
            {
                Matrix cameraRotation = Matrix.CreateRotationX(angles.X) * Matrix.CreateRotationY(angles.Y);
                return position + Vector3.Transform(Vector3.Forward, cameraRotation);
            }
            set
            {
                Vector3 forward = Vector3.Normalize(position - value);
                Vector3 right = Vector3.Normalize(Vector3.Cross(forward, Vector3.Up));
                Vector3 up = Vector3.Normalize(Vector3.Cross(right, forward));

                Matrix test = Matrix.Identity;
                test.Forward = forward;
                test.Right = right;
                test.Up = up;
                angles.X = -(float)Math.Asin(test.M32);
                angles.Y = -(float)Math.Asin(test.M13);

            }
        }

        /// <summary>
        /// Updates camera with input and updates view matrix.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Update(GameTime gameTime)
        {
            if (Enabled)
            {
                double elapsedTime = (double)gameTime.ElapsedGameTime.Ticks / (double)TimeSpan.TicksPerSecond;
                ProcessInput((float)elapsedTime * 50.0f);

                UpdateView();
                UpdateInput();

                base.Update(gameTime);
            }
        }

        private void ProcessInput(float amountOfMovement)
        {
            Vector3 moveVector = new Vector3();

            Matrix cameraRotation = Matrix.CreateRotationX(angles.X) * Matrix.CreateRotationY(angles.Y);

            position += Vector3.Transform(moveVector, cameraRotation);
            //Move(position.X, position.Y, position.Z);



            if ((EnableMouseInput) && (!((sqengine)_game).IsGuiActive) && (!((sqengine)_game).IsInputEnabled))
            {
                MouseState currentMouseState = Mouse.GetState();

                if (currentMouseState.RightButton == ButtonState.Pressed && prevMouseState.RightButton == ButtonState.Released && IsMousePinned)
                {
                    //Mouse.SetPosition(widthOver2, heightOver2);
                }
                else if (currentMouseState.RightButton == ButtonState.Pressed || IsMousePinned)
                {
                    if (currentMouseState.X != widthOver2)
                        angles.Y -= amountOfMovement / 80.0f * (currentMouseState.X - widthOver2);
                    if (currentMouseState.Y != heightOver2)
                        angles.X -= amountOfMovement / 80.0f * (currentMouseState.Y - heightOver2);

                    if (angles.X > 1.4) angles.X = 1.4f;
                    if (angles.X < -1.4) angles.X = -1.4f;
                    if (angles.Y > Math.PI) angles.Y -= 2 * (float)Math.PI;
                    if (angles.Y < -Math.PI) angles.Y += 2 * (float)Math.PI;
                }

                prevMouseState = currentMouseState;
            }


            UpdateInput();
        }

        private void UpdateProjection()
        {
            projection = Matrix.CreatePerspectiveFieldOfView(fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance);

            Perspective(fieldOfView, aspectRatio, nearPlaneDistance, farPlaneDistance);

        }

        public void RefreshAspectRatio(Game game)
        {
            widthOver2 = game.Window.ClientBounds.Width / 2;
            heightOver2 = game.Window.ClientBounds.Height / 2;
            aspectRatio = (float)game.Window.ClientBounds.Width / (float)game.Window.ClientBounds.Height;

            UpdateProjection();
        }

        public void Perspective(float fovx, float aspect, float znear, float zfar)
        {
            this.fovx = fovx;
            this.aspectRatio = aspect;
            this.znear = znear;
            this.zfar = zfar;

            float aspectInv = 1.0f / aspect;
            float e = 1.0f / (float)Math.Tan(MathHelper.ToRadians(fovx) / 2.0f);
            float fovy = 2.0f * (float)Math.Atan(aspectInv / e);
            float xScale = 1.0f / (float)Math.Tan(0.5f * fovy);
            float yScale = xScale / aspectInv;

            projMatrix.M11 = xScale;
            projMatrix.M12 = 0.0f;
            projMatrix.M13 = 0.0f;
            projMatrix.M14 = 0.0f;

            projMatrix.M21 = 0.0f;
            projMatrix.M22 = yScale;
            projMatrix.M23 = 0.0f;
            projMatrix.M24 = 0.0f;

            projMatrix.M31 = 0.0f;
            projMatrix.M32 = 0.0f;
            projMatrix.M33 = (zfar + znear) / (znear - zfar);
            projMatrix.M34 = -1.0f;

            projMatrix.M41 = 0.0f;
            projMatrix.M42 = 0.0f;
            projMatrix.M43 = (2.0f * zfar * znear) / (znear - zfar);
            projMatrix.M44 = 0.0f;
        }



        private void UpdateView()
        {
            cameraRotation = Matrix.CreateRotationX(angles.X) * Matrix.CreateRotationY(angles.Y);
            Vector3 targetPos = position + Vector3.Transform(Vector3.Forward, cameraRotation);

            Vector3 upVector = Vector3.Transform(Vector3.Up, cameraRotation);



            xAxis.X = viewMatrix.M11;
            xAxis.Y = viewMatrix.M21;
            xAxis.Z = viewMatrix.M31;

            yAxis.X = viewMatrix.M12;
            yAxis.Y = viewMatrix.M22;
            yAxis.Z = viewMatrix.M32;

            zAxis.X = viewMatrix.M13;
            zAxis.Y = viewMatrix.M23;
            zAxis.Z = viewMatrix.M33;

            viewMatrix.M41 = -Vector3.Dot(xAxis, eye);
            viewMatrix.M42 = -Vector3.Dot(yAxis, eye);
            viewMatrix.M43 = -Vector3.Dot(zAxis, eye);

            viewDir.X = -zAxis.X;
            viewDir.Y = -zAxis.Y;
            viewDir.Z = -zAxis.Z;

            Matrix.CreateFromQuaternion(ref orientation, out viewMatrix);


            view = Matrix.CreateLookAt(position, targetPos, upVector);
            viewMatrix = Matrix.CreateLookAt(position, targetPos, upVector);
            //LookAt(targetPos);


            UpdateProjection();

            LookAt(position, targetPos, upVector);
        }



        public void LookAt(Vector3 target)
        {
            LookAt(eye, target, yAxis);
        }

        public void LookAt(Vector3 eye, Vector3 target, Vector3 up)
        {
            this.eye = eye;
            this.target = target;

            zAxis = eye - target;
            zAxis.Normalize();

            viewDir.X = -zAxis.X;
            viewDir.Y = -zAxis.Y;
            viewDir.Z = -zAxis.Z;

            Vector3.Cross(ref up, ref zAxis, out xAxis);
            xAxis.Normalize();

            Vector3.Cross(ref zAxis, ref xAxis, out yAxis);
            yAxis.Normalize();
            xAxis.Normalize();

            viewMatrix.M11 = xAxis.X;
            viewMatrix.M21 = xAxis.Y;
            viewMatrix.M31 = xAxis.Z;
            Vector3.Dot(ref xAxis, ref eye, out viewMatrix.M41);
            viewMatrix.M41 = -viewMatrix.M41;

            viewMatrix.M12 = yAxis.X;
            viewMatrix.M22 = yAxis.Y;
            viewMatrix.M32 = yAxis.Z;
            Vector3.Dot(ref yAxis, ref eye, out viewMatrix.M42);
            viewMatrix.M42 = -viewMatrix.M42;

            viewMatrix.M13 = zAxis.X;
            viewMatrix.M23 = zAxis.Y;
            viewMatrix.M33 = zAxis.Z;
            Vector3.Dot(ref zAxis, ref eye, out viewMatrix.M43);
            viewMatrix.M43 = -viewMatrix.M43;

            viewMatrix.M14 = 0.0f;
            viewMatrix.M24 = 0.0f;
            viewMatrix.M34 = 0.0f;
            viewMatrix.M44 = 1.0f;

            accumPitchDegrees = MathHelper.ToDegrees((float)Math.Asin(viewMatrix.M23));
            accumHeadingDegrees = MathHelper.ToDegrees((float)Math.Atan2(viewMatrix.M13, viewMatrix.M33));

            Quaternion.CreateFromRotationMatrix(ref viewMatrix, out orientation);
        }

        public void Rotate(float headingDegrees, float pitchDegrees)
        {
            headingDegrees = -headingDegrees;
            pitchDegrees = -pitchDegrees;

            accumPitchDegrees += pitchDegrees;

            if (accumPitchDegrees > 90.0f)
            {
                pitchDegrees = 90.0f - (accumPitchDegrees - pitchDegrees);
                accumPitchDegrees = 90.0f;
            }

            if (accumPitchDegrees < -90.0f)
            {
                pitchDegrees = -90.0f - (accumPitchDegrees - pitchDegrees);
                accumPitchDegrees = -90.0f;
            }

            accumHeadingDegrees += headingDegrees;

            if (accumHeadingDegrees > 360.0f)
                accumHeadingDegrees -= 360.0f;

            if (accumHeadingDegrees < -360.0f)
                accumHeadingDegrees += 360.0f;

            float heading = MathHelper.ToRadians(headingDegrees);
            float pitch = MathHelper.ToRadians(pitchDegrees);
            Quaternion rotation = Quaternion.Identity;

            // Rotate the camera about the world Y axis.
            if (heading != 0.0f)
            {
                Quaternion.CreateFromAxisAngle(ref WORLD_Y_AXIS, heading, out rotation);
                Quaternion.Concatenate(ref rotation, ref orientation, out orientation);
            }

            // Rotate the camera about its local X axis.
            if (pitch != 0.0f)
            {
                Quaternion.CreateFromAxisAngle(ref WORLD_X_AXIS, pitch, out rotation);
                Quaternion.Concatenate(ref orientation, ref rotation, out orientation);
            }

            UpdateView();
        }


        private void PerformMouseSmoothing(float x, float y)
        {
            mouseMovement[mouseIndex].X = x;
            mouseMovement[mouseIndex].Y = y;

            smoothedMouseMovement.X = (mouseMovement[0].X + mouseMovement[1].X) * 0.5f;
            smoothedMouseMovement.Y = (mouseMovement[0].Y + mouseMovement[1].Y) * 0.5f;

            mouseIndex ^= 1;
            mouseMovement[mouseIndex].X = 0.0f;
            mouseMovement[mouseIndex].Y = 0.0f;
        }



        private void UpdateInput()
        {

            previousMouseState = currentMouseState;
            currentMouseState = Mouse.GetState();

            Rectangle clientBounds = Game.Window.ClientBounds;

            int centerX = clientBounds.Width / 2;
            int centerY = clientBounds.Height / 2;
            int deltaX = centerX - currentMouseState.X;
            int deltaY = centerY - currentMouseState.Y;

            if (IsMousePinned && !((sqengine)_game).IsGuiActive && !((sqengine)_game).IsInputEnabled)
                Mouse.SetPosition(centerX, centerY);


            //smoothedMouseMovement.X = (float)deltaX;
            //smoothedMouseMovement.Y = (float)deltaY;

        }

        public void Move(float dx, float dy, float dz)
        {
            // Calculate the forwards direction. Can't just use the
            // camera's view direction as doing so will cause the camera to
            // move more slowly as the camera's view approaches 90 degrees
            // straight up and down.

            Vector3 forwards = Vector3.Normalize(Vector3.Cross(WORLD_Y_AXIS, xAxis));

            eye += xAxis * dx;
            eye += WORLD_Y_AXIS * dy;
            eye += forwards * dz;

            Position = eye;
        }

        public Matrix WeaponWorldMatrix(float xOffset, float yOffset, float zOffset)
        {
            Vector3 weaponPos = eye;

            weaponPos += viewDir * zOffset;
            weaponPos += yAxis * yOffset;
            weaponPos += xAxis * xOffset;

            return Matrix.CreateRotationX(angles.X)
                    * Matrix.CreateRotationY(angles.Y)
                    * Matrix.CreateTranslation(weaponPos);
        }

        /// <summary>
        /// Calculates the world transformation matrix for the weapon attached
        /// to the FirstPersonCamera. The weapon moves along with the camera.
        /// The offsets are to ensure the weapon is slightly in front of the
        /// camera and to one side.
        /// </summary>
        /// <param name="xOffset">How far to position the weapon left or right.</param>
        /// <param name="yOffset">How far to position the weapon up or down.</param>
        /// <param name="zOffset">How far to position the weapon in front or behind.</param>
        /// <param name="scale">How much to scale the weapon.</param>
        /// <returns>The weapon world transformation matrix.</returns>
        public Matrix WeaponWorldMatrix(float xOffset, float yOffset, float zOffset, float scale)
        {
            Vector3 weaponPos = position;

            weaponPos += viewDir * zOffset;
            weaponPos += yAxis * yOffset;
            weaponPos += xAxis * xOffset;

            return Matrix.CreateScale(scale)
                * Matrix.CreateRotationX(angles.X)
                * Matrix.CreateRotationY(angles.Y)
                * Matrix.CreateTranslation(weaponPos);
        }

        public Matrix ObjectWorldMatrix(float xOffset, float yOffset, float zOffset, float scale, Vector3 myposition)
        {
            Vector3 weaponPos = myposition;

            weaponPos += viewDir * zOffset;
            weaponPos += yAxis * yOffset;
            weaponPos += xAxis * xOffset;

            return Matrix.CreateScale(scale)
                * Matrix.CreateRotationX(angles.X)
                * Matrix.CreateRotationY(angles.Y)
                * Matrix.CreateTranslation(weaponPos);
        }

        public void EnableWireframe(bool state)
        {
            if (state)
            {

                isWireframeEnabled = !isWireframeEnabled;


                if (isWireframeEnabled)
                {
                    _game.GraphicsDevice.RenderState.FillMode = FillMode.WireFrame;

                }
                else
                {
                    _game.GraphicsDevice.RenderState.FillMode = FillMode.Solid;
                }
            }
        }

        public Matrix UpdateReflectiveMatrices()
        {
            Matrix reflectiveViewMatrix;
            reflectiveViewMatrix = Matrix.Identity;
            reflectiveViewMatrix *= Matrix.CreateTranslation(new Vector3(-position.X, position.Y - 10f, -position.Z));
            //reflectiveViewMatrix *= Matrix.CreateRotationZ(angles.Z);
            reflectiveViewMatrix *= Matrix.CreateRotationY(angles.Y);
            reflectiveViewMatrix *= Matrix.CreateRotationX(-angles.X);
            return reflectiveViewMatrix;
        }




    }
}
